package net.wanderinghorse.jsonmessage;

import net.wanderinghorse.json.AbstractJSONable;
import net.wanderinghorse.json.JSONException;
import net.wanderinghorse.json.JSONObject;

import java.io.Serializable;
import java.util.logging.Logger;

/**
 * This class contains properties which are used as connection-related
 * options for posting Requests member. It is intended to model, as closely
 * as possible, the post options interface specified for the JavaScript
 * side of the API.
 *
 * This class SHOULD be an inner class of Request, but Java applies
 * restrictions which hinder that (e.g. no static members in
 * inner classes).
 *
 * Note that while this class is JSONable, it does not get serialized
 * along with its associated Request object. It is primarily JSONable for
 * debugging purposes, or for use in de/serializing the default post() options
 * for a given application. Note that only properties which are "plain old
 * data" (strings, numbers, and booleans) get JSONized. Callbacks and such
 * are not included in the JSON form of these objects.
 *
 * TODO: abstract the properties a bit, to allow custom properties which
 * are useful for specific RequestPoster implementations. The current impl
 * is very HTTP-centric and closely mimics the JS-side API.
 */
public class PostOptions extends AbstractJSONable implements Serializable
{
    private static final long serialVersionUID = 1L;
    /**
     * The post() options property keys specified by the JS API. The values
     * here are the strings required by that API, documented in the library
     * manual, etc.
     *
     * TODO: make this an enum instead of a set of strings.
     */

    public enum Key {
        URL("url"),
        CONNECT_METHOD("method"),
        TIMEOUT("timeout"),
        LOGIN_NAME("loginName"),
        LOGIN_PASSWORD("loginPassword"),
        ASYNC("asynchronous"),
        ERROR_MESSAGE("errorMessage"),
        BEFORE_POST("beforePost"),
        AFTER_POST("afterPost")
        //JSONP("jsonpCallback")
        ;
        private String value;
        private Key(final String v) {
            value = v;
        }
        public String toString() {
            return value;
        }
    }
    /**
     * Default connection timeout value.
     */
    public static final int DefaultTimeout = 15000;

    /**
     * Initializes all properties to null values.
     */
    public PostOptions() {
    }

    private String url = null;
    private String connectMethod = null;
    private Integer timeout = null;
    private Boolean async = null;
    private String loginName = null;
    private String loginPassword = null;
    //private String jsonpCallback = null;

    private String errorMessage = null;
    private CallbackRequestOpt onError = null;
    private CallbackRequestOpt beforePost = null;
    private CallbackRequestOpt afterPost = null;
    private RequestHandler onResponse = null;


    /**
     * Sets all properties in this object to null values.
     */
    public void clear() {
        url = null;
        connectMethod = null;
        timeout = null;
        async = null;
        loginName = null;
        loginPassword = null;
        errorMessage = null;
        onError = null;
        beforePost = null;
        afterPost = null;
        onResponse = null;
    }

    @Override
    public JSONObject toJSONObject() throws JSONException {
        JSONObject jo = new JSONObject();
        if( null != this.url ) jo.put( "url", this.url );
        jo.put( Key.CONNECT_METHOD.toString(), this.connectMethod);
        jo.put( Key.TIMEOUT.toString(), this.timeout );
        jo.put( Key.LOGIN_NAME.toString(), this.loginName );
        jo.put( Key.LOGIN_PASSWORD.toString(), this.loginPassword );
        jo.put( Key.ASYNC.toString(), this.async );
        jo.put( Key.ERROR_MESSAGE.toString(), this.errorMessage );
        //jo.put( Key.JSONP.toString(), this.jsonpCallback );
        return jo;
    }

    @Override
    public void fromJSONObject(JSONObject src) throws JSONException {
        this.clear();
        this.url = src.optString(Key.URL.toString(),null);
        this.connectMethod = src.optString(Key.CONNECT_METHOD.toString(),null);
        this.loginName = src.optString( Key.LOGIN_NAME.toString(),null);
        this.loginPassword = src.optString( Key.LOGIN_PASSWORD.toString(),null);
        this.errorMessage = src.optString( Key.ERROR_MESSAGE.toString(),null);
        //this.jsonpCallback = src.optString( Key.JSONP.toString(),null);
        // Need special handling for JSONObject funcs which take primitives:
        if( src.has(Key.TIMEOUT.toString()) ) this.timeout = src.getInt( Key.TIMEOUT.toString() );
        if( src.has(Key.ASYNC.toString()) ) this.async = src.getBoolean( Key.ASYNC.toString() );
    }


    public String getErrorMessage() {
        return errorMessage;
    }

    /**
     * This should only be set by post() implementations when they encounter
     * an error, before calling their onError() callback.
     *
     * @param errorMessage An error message.
     * @return This object.
     */
    public PostOptions setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
        return this;
    }

    public String getUrl() {
        return url;
    }

    /**
     * Sets the URL of the remote post() dispatcher. Note that this is a String
     * instead of a java.net.URL primarily for symmetry with the JS API.
     * 
     * @param url The URL of the remote dispatcher.
     * @return This object.
     */
    public PostOptions setUrl( final String url ) {
        this.url = url;
        return this;
    }

    public String getConnectMethod() {
        return connectMethod;
    }

    /**
     * Sets the "connection method". For HTTP-based connections this should
     * be "POST", but implementations may also support "GET". Non-HTTP implementations
     * may ignore this or expect other values.
     * @param m The connection method string.
     * @return This object.
     */
    public PostOptions setConnectMethod( final String m ) {
        this.connectMethod = m;
        return this;
    }

    public Integer getTimeout() {
        return timeout;
    }

    public PostOptions setTimeout( final Integer timeout ) {
        this.timeout = timeout;
        return this;
    }

    public Boolean isAsync() {
        return async;
    }

    /**
     * Sets whether the connection should be asynchronous or synchronous.
     * Not all post() implementations support both modes.
     * @param async Whether or not to run asynchronously.
     * @return This object.
     */
    public PostOptions setAsync( final Boolean async ) {
        this.async = async;
        return this;
    }

    public String getLoginName() {
        return loginName;
    }

    public PostOptions setLoginName(String loginName) {
        this.loginName = loginName;
        return this;
    }

    public String getLoginPassword() {
        return loginPassword;
    }

    public PostOptions setLoginPassword(String loginPassword) {
        this.loginPassword = loginPassword;
        return this;
    }

    public CallbackRequestOpt getOnError() {
        return onError;
    }

    public PostOptions setOnError(CallbackRequestOpt onError) {
        this.onError = onError;
        return this;
    }

    public CallbackRequestOpt getBeforePost() {
        return beforePost;
    }

    public PostOptions setBeforePost(CallbackRequestOpt beforePost) {
        this.beforePost = beforePost;
        return this;
    }

    public CallbackRequestOpt getAfterPost() {
        return this.afterPost;
    }

    public PostOptions setAfterPost(CallbackRequestOpt afterPost) {
        this.afterPost = afterPost;
        return this;
    }

    public RequestHandler getOnResponse() {
        return onResponse;
    }

    public PostOptions setOnResponse(RequestHandler onResponse) {
        this.onResponse = onResponse;
        return this;
    }

//    public String getJsonpCallback() {
//        return this.jsonpCallback;
//    }

    /**
     * RequestPoster implementations which send to remote hosts over HTTP
     * should, if this value is set, add a GET/POST argument named Key.JSONP
     * with the value passed here.
     *
     * @param callback The JSONP callback function name which the back-end
     * @return
     */
//    public PostOptions setJsonpCallback(String callback) {
//        this.jsonpCallback = callback;
//        return this;
//    }

    /**
     * Dest is modified such that any of its properties which are null are copied from src.
     * Note that src and dest will share the same references to the copied data.
     *
     * @param src The source object to pull missing properties from.
     * @param dest The object to check for unset properties, and set them from src if
     * they are null in the dest object.
     * @return The dest object.
     */
    public static PostOptions mergeMissing( final PostOptions src, final PostOptions dest ) {
        if( src == dest ) return dest;
        if( null == dest.getAfterPost() ) dest.setAfterPost( src.getAfterPost() );
        if( null == dest.getBeforePost() ) dest.setBeforePost( src.getBeforePost() );
        if( null == dest.getOnResponse() ) dest.setOnResponse( src.getOnResponse() );
        if( null == dest.getOnError() ) dest.setOnError( src.getOnError() );
        if( null == dest.getTimeout() ) dest.setTimeout( src.getTimeout() );
        if( null == dest.getLoginName() ) dest.setLoginName( src.getLoginName() );
        if( null == dest.getLoginPassword() ) dest.setLoginPassword( src.getLoginPassword() );
        if( null == dest.getUrl() ) dest.setUrl( src.getUrl() );
        if( null == dest.isAsync() ) dest.setAsync( src.isAsync() );
        //if( null == dest.getJsonpCallback() ) dest.setJsonpCallback( src.getJsonpCallback() );
        return dest;
    }

    /**
     * All properties of src which are not null are copied into dest.
     * Note that src and dest will share the same references to the copied data.
     *
     * @param src The source object.
     * @param dest The destination object.
     * @return The dest object.
     */
    public static PostOptions mergeNonNull( final PostOptions src, final PostOptions dest ) {
        if( src == dest ) return dest;
        if( null != src.getAfterPost() ) dest.setAfterPost( src.getAfterPost() );
        if( null != src.getBeforePost() ) dest.setBeforePost( src.getBeforePost() );
        if( null != src.getOnResponse() ) dest.setOnResponse( src.getOnResponse() );
        if( null != src.getOnError() ) dest.setOnError( src.getOnError() );
        if( null != src.getTimeout() ) dest.setTimeout( src.getTimeout() );
        if( null != src.getLoginName() ) dest.setLoginName( src.getLoginName() );
        if( null != src.getLoginPassword() ) dest.setLoginPassword( src.getLoginPassword() );
        if( null != src.getUrl() ) dest.setUrl( src.getUrl() );
        if( null != src.isAsync() ) dest.setAsync( src.isAsync() );
        //if( null != src.getJsonpCallback() ) dest.setJsonpCallback( src.getJsonpCallback() );
        return dest;
    }

    /**
     * Creates a PostOptions object (which the caller owns) containing defaults
     * for certain values. The fields (timeout, asynchronous, connectMethod)
     * are set to sensible defaults and (onError) is set to a handler which
     * simply echoes its output to System.out.
     *
     * Note that this produces different results than the default constructor.
     *
     * @return The new PostOptions object.
     */
    public static PostOptions createDefault() {
        PostOptions po = new PostOptions();
        po.setTimeout( PostOptions.DefaultTimeout );
        po.setAsync( false );
        po.setConnectMethod("POST");
        po.setOnError( new CallbackRequestOptToPrintStream(System.out) );
        return po;
    }
}
